﻿using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Web;
using System.Web.Hosting;
using System.Web.Mvc;
using System.Web.Routing;
using System.Web.WebPages.Razor;

namespace Quick.PluginFramework.ViewEngines
{
    public abstract class ThemeableVirtualPathProviderViewEngine : IViewEngine
    {
        private const string _cacheKeyFormat = ":ViewCacheEntry:{0}:{1}:{2}:{3}:{4}:{5}:";
        private const string _cacheKeyPrefix_Master = "Master";
        private const string _cacheKeyPrefix_Partial = "Partial";
        private const string _cacheKeyPrefix_View = "View";

        #region Fields

        public string[] PluginViewLocationFormats { get; set; }
        public string[] PluginMasterLocationFormats { get; set; }
        public string[] PluginPartialViewLocationFormats { get; set; }

        public string[] AreaMasterLocationFormats { get; set; }

        public string[] AreaPartialViewLocationFormats { get; set; }

        public string[] AreaViewLocationFormats { get; set; }

        public string[] MasterLocationFormats { get; set; }

        public string[] PartialViewLocationFormats { get; set; }

        public string[] ViewLocationFormats { get; set; }

        /// <summary>
        /// Valid page extensions for the view engine
        /// </summary>
        public string[] PageExtensions { get; set; }    // DEGT globalization

        public IViewLocationCache ViewLocationCache { get; set; }
        private readonly string[] _emptyLocations = null;

        /// <summary>
        /// 样式
        /// </summary>

        public Func<HttpContextBase, string> CurrentTheme { get; set; }

        private VirtualPathProvider _vpp;
        protected VirtualPathProvider VirtualPathProvider
        {
            get
            {
                return _vpp ?? (_vpp = HostingEnvironment.VirtualPathProvider);
            }

            set
            {
                _vpp = value;
            }
        }

        #endregion

        #region Ctor

        protected ThemeableVirtualPathProviderViewEngine()
        {
            if (HttpContext.Current == null || HttpContext.Current.IsDebuggingEnabled)
            {
                ViewLocationCache = DefaultViewLocationCache.Null;
            }
            else
            {
                ViewLocationCache = new DefaultViewLocationCache();
            }
        }

        #endregion

        #region Utilities

        /// <summary>
        /// 
        /// </summary>
        /// <param name="controllerContext">ControllerContext</param>
        /// <param name="locations">Locations</param>
        /// <param name="areaLocations">AreaLocations</param>
        /// <param name="pluginLocations">PluginLocations</param>
        /// <param name="locationsPropertyName">LocationsPropertyName</param>
        /// <param name="name">Name</param>
        /// <param name="controllerName">ControllerName</param>
        /// <param name="cacheKeyPrefix">CacheKeyPrefix</param>
        /// <param name="useCache">UseCache</param>
        /// <param name="checkPathValidity">CheckPathValidity</param>
        /// <param name="incompleteMatch">IncompleteMatch</param>
        /// <param name="searchedLocations">SearchedLocations</param>
        /// <returns></returns>
        protected virtual string GetPath(ControllerContext controllerContext, IEnumerable<string> locations, IEnumerable<string> areaLocations,
            IEnumerable<string> pluginLocations, string locationsPropertyName, string name, string controllerName, string cacheKeyPrefix,
            bool useCache, bool checkPathValidity, ref bool incompleteMatch, out string[] searchedLocations)
        {
            searchedLocations = _emptyLocations;
            if (string.IsNullOrEmpty(name))
            {
                return string.Empty;
            }
            //检查是否是参数是否是插件
            string pluginName = GetPluginName(controllerContext.RouteData);
            bool usingPlugin = !string.IsNullOrEmpty(pluginName);
            if (usingPlugin)
            {
                this.CodeGeneration(pluginName);
            }
            //检查是否是areas
            string areaName = GetAreaName(controllerContext.RouteData);
            bool usingAreas = !string.IsNullOrEmpty(areaName);

            string theme = CurrentTheme(controllerContext.HttpContext);

            if (string.IsNullOrEmpty(areaName))
            {
                areaName = pluginName;
                pluginName = string.Empty;
            }

            List<ViewLocation> viewLocations = GetViewLocations(locations, (usingAreas) ? areaLocations : null, (usingPlugin) ? pluginLocations : null);

            if (viewLocations.Count == 0)
            {
                throw new InvalidOperationException(string.Format(CultureInfo.CurrentCulture, "The property '{0}' cannot be null or empty.", locationsPropertyName));
            }

            bool nameRepresentsPath = IsSpecificPath(name);

            string cacheKey = this.CreateCacheKey(cacheKeyPrefix, name, nameRepresentsPath ? string.Empty : controllerName, areaName,
                theme);
            if (useCache)
            {
                var cached = this.ViewLocationCache.GetViewLocation(controllerContext.HttpContext, cacheKey);
                if (cached != null)
                {
                    return cached;
                }
            }
            return nameRepresentsPath ?
                GetPathFromSpecificName(controllerContext, name, cacheKey, checkPathValidity, ref searchedLocations, ref incompleteMatch) :
                GetPathFromGeneralName(controllerContext, viewLocations, name, controllerName, areaName, theme, cacheKey, ref searchedLocations);
        }

        protected virtual string GetPathFromSpecificName(ControllerContext controllerContext, string name, string cacheKey,
            bool checkPathValidity, ref string[] searchedLocations, ref bool incompleteMatch)
        {
            string virtualPath = name;
            bool fileExists = FileExists(controllerContext, name);

            if (checkPathValidity && fileExists)
            {
                bool? validPath = IsValidPath(controllerContext, name);

                if (validPath == false)
                {
                    fileExists = false;
                }
                else if (validPath == null)
                {
                    incompleteMatch = true;
                }
            }
            if (!fileExists)
            {
                virtualPath = string.Empty;
                searchedLocations = new[] { name };
            }
#if LOCALIZE
            else
            {
                // View was located, not lets look on that same location for localized versions
                // of the same view
                string localizedVirtualPath;
                if (LocalizedFileExists(controllerContext, virtualPath, out localizedVirtualPath))
                {
                    virtualPath = localizedVirtualPath;
                }
            }
#endif
            if (!incompleteMatch)
            {
                ViewLocationCache.InsertViewLocation(controllerContext.HttpContext, cacheKey, virtualPath);
            }

            return virtualPath;
        }
     
        protected virtual string GetPathFromGeneralName(ControllerContext controllerContext,
            List<ViewLocation> locations, string name, string controllerName, string areaName, string theme,
            string cacheKey, ref string[] searchedLocations)
        {
            string virtualPath = string.Empty;
            searchedLocations = new string[locations.Count];
            for (int i = 0; i < locations.Count; i++)
            {
                string str2 = locations[i].Format(name, controllerName, areaName, theme);
                if (this.FileExists(controllerContext, str2))
                {
                    searchedLocations = _emptyLocations;
                    virtualPath = str2;
                    this.ViewLocationCache.InsertViewLocation(controllerContext.HttpContext, cacheKey, virtualPath);
                    return virtualPath;
                }
                searchedLocations[i] = str2;
            }
            return virtualPath;
        }

        protected virtual string CreateCacheKey(string prefix, string name, string controllerName, string areaName,
            string theme)
        {
            return string.Format(CultureInfo.InvariantCulture, _cacheKeyFormat,
                new object[] { base.GetType().AssemblyQualifiedName, prefix, name, controllerName, areaName, theme });
        }

        protected virtual List<ViewLocation> GetViewLocations(IEnumerable<string> viewLocationFormats,
            IEnumerable<string> areaViewLocationFormats = null, IEnumerable<string> pluginLocationFormats = null)
        {
            var allLocations = new List<ViewLocation>();
            if (areaViewLocationFormats != null)
            {
                allLocations.AddRange(areaViewLocationFormats.Select(areaViewLocationFormat => new AreaAwareViewLocation(areaViewLocationFormat)));
            }
            if (pluginLocationFormats != null)
            {
                allLocations.AddRange(pluginLocationFormats.Select(pluginLocationFormat => new AreaAwareViewLocation(pluginLocationFormat)));
            }
            if (viewLocationFormats != null)
            {
                allLocations.AddRange(viewLocationFormats.Select(viewLocationFormat => new ViewLocation(viewLocationFormat)));
            }
            return allLocations;
        }

        protected virtual bool IsSpecificPath(string name)
        {
            char ch = name[0];
            if (ch != '~')
            {
                return (ch == '/');
            }
            return true;
        }

        protected virtual string GetPluginName(RouteData routeData)
        {
            if (routeData.Values.ContainsKey("plugin"))
            {
                var pluginName = routeData.GetRequiredString("plugin");
                return pluginName;
            }
            return GetPluginName(routeData.Route);
        }

        protected virtual string GetPluginName(RouteBase route)
        {
            var area = route as IRouteWithArea;
            if (area != null)
            {
                return area.Area;
            }
            var route2 = route as Route;
            if ((route2 != null) && (route2.DataTokens != null) && (route2.DataTokens.ContainsKey("plugin")))
            {
                System.Diagnostics.Trace.WriteLine(route2.DataTokens["plugin"].ToString());
                return (route2.DataTokens["plugin"] as string);
            }

            return null;
        }

        protected virtual string GetAreaName(RouteData routeData)
        {
            object obj2;
            if (routeData.DataTokens.TryGetValue("area", out obj2))
            {
                return (obj2 as string);
            }
            return GetAreaName(routeData.Route);
        }

        protected virtual string GetAreaName(RouteBase route)
        {
            var area = route as IRouteWithArea;
            if (area != null)
            {
                return area.Area;
            }
            var route2 = route as Route;
            if ((route2 != null) && (route2.DataTokens != null) && (route2.DataTokens.ContainsKey("plugin")))
            {
                route2.DataTokens["area"] = route2.DataTokens["plugin"];
                System.Diagnostics.Trace.WriteLine(route2.DataTokens["plugin"].ToString());
                return (route2.DataTokens["plugin"] as string);
                //System.Diagnostics.Trace.WriteLine(route2.DataTokens["plugin"].ToString());
                //return (route2.DataTokens["area"] as string);
            }

            return null;
        }

        protected virtual ViewEngineResult FindThemeableView(ControllerContext controllerContext, string viewName,
            string masterName, bool useCache)
        {

            if (controllerContext == null)
            {
                throw new ArgumentNullException("controllerContext");
            }
            if (string.IsNullOrEmpty(viewName))
            {
                throw new ArgumentException("Value cannot be null or empty.", "viewName");
            }
            string[] viewLocationsSearched;
            string[] masterLocationsSearched;
            bool incompleteMatch = false;

            string controllerName = controllerContext.RouteData.GetRequiredString("controller");
            string viewPath = this.GetPath(controllerContext, ViewLocationFormats, AreaViewLocationFormats, PluginViewLocationFormats,
                "ViewLocationFormats", viewName, controllerName, _cacheKeyPrefix_View, useCache,
                /* checkPathValidity */ true, ref incompleteMatch, out viewLocationsSearched);

            string masterPath = this.GetPath(controllerContext, MasterLocationFormats, AreaMasterLocationFormats, PluginMasterLocationFormats,
                "MasterLocationFormats", masterName, controllerName, _cacheKeyPrefix_Master, useCache,
                /* checkPathValidity */ false, ref incompleteMatch, out masterLocationsSearched);
            if (string.IsNullOrEmpty(viewPath) || (string.IsNullOrEmpty(masterPath) && !string.IsNullOrEmpty(masterName)))
            {
                return new ViewEngineResult(viewLocationsSearched.Union(masterLocationsSearched));
            }
            return new ViewEngineResult(CreateView(controllerContext, viewPath, masterPath), this); //, incompleteMatch);
        }

        protected virtual ViewEngineResult FindThemeablePartialView(ControllerContext controllerContext,
            string partialViewName, bool useCache)
        {

            if (controllerContext == null)
            {
                throw new ArgumentNullException("controllerContext");
            }
            if (string.IsNullOrEmpty(partialViewName))
            {
                throw new ArgumentException("Value cannot be null or empty.", "partialViewName");
            }
            string[] searched;
            bool incompleteMatch = false;

            string controllerName = controllerContext.RouteData.GetRequiredString("controller");

            string partialPath = this.GetPath(controllerContext, PartialViewLocationFormats, AreaPartialViewLocationFormats,
                PluginPartialViewLocationFormats, "PartialViewLocationFormats", partialViewName, controllerName,
                _cacheKeyPrefix_Partial, useCache, /* checkBaseType */ true, ref incompleteMatch, out searched);


            if (string.IsNullOrEmpty(partialPath))
            {
                return new ViewEngineResult(searched);
            }
            return new ViewEngineResult(CreatePartialView(controllerContext, partialPath), this);
        }

        /// <summary>
        /// 给运行时编译的页面加了引用程序集。
        /// </summary>
        /// <param name="pluginName"></param>
        private void CodeGeneration(string pluginName)
        {
            RazorBuildProvider.CodeGenerationStarted += (object sender, EventArgs e) =>
            {
                RazorBuildProvider provider = (RazorBuildProvider)sender;

                var plugin = PluginManager.GetPlugin(pluginName);

                if (plugin != null)
                {
                    provider.AssemblyBuilder.AddAssemblyReference(plugin.Assembly);
                    if (plugin.DependentAssemblys != null)
                    {
                        foreach (var assem in plugin.DependentAssemblys)
                        {
                            provider.AssemblyBuilder.AddAssemblyReference(assem);
                        }
                    }
                }
            };
        }

        public virtual void ReleaseView(ControllerContext controllerContext, IView view)
        {
            var disposable = view as IDisposable;

            if (disposable != null)
            {
                disposable.Dispose();
            }
        }

        protected virtual bool FileExists(ControllerContext controllerContext, string virtualPath)
        {
            return VirtualPathProvider.FileExists(virtualPath);
        }
        #endregion

        #region Methods

        public virtual ViewEngineResult FindView(ControllerContext controllerContext, string viewName,
            string masterName, bool useCache)
        {
            ViewEngineResult result = FindThemeableView(controllerContext, viewName, masterName, useCache);
            return result;

        }

        public virtual ViewEngineResult FindPartialView(ControllerContext controllerContext, string partialViewName,
            bool useCache)
        {
            ViewEngineResult result = FindThemeablePartialView(controllerContext, partialViewName, useCache);
            return result;
        }

        protected abstract IView CreatePartialView(ControllerContext controllerContext, string partialPath);

        protected abstract IView CreateView(ControllerContext controllerContext, string viewPath, string masterPath);

        protected virtual bool? IsValidPath(ControllerContext controllerContext, string virtualPath)
        {
            return null;
        }

        #endregion



#if LOCALIZE
        /// <summary>
        /// Localization extension that is called when the View is located so that we then start looking on the
        /// same location for alternate/localized versions of the same file. If the view found was Index.cshtml
        /// then it would use the Browser-Settings or thread UICulture settings to find the same view but with
        /// a language code, for examle Index.es-ES.cshtml or Index.nl.cshtml
        /// </summary>
        /// <param name="controllerContext"></param>
        /// <param name="fallbackVirtualPath"></param>
        /// <param name="localizedVirtualPath"></param>
        /// <returns></returns>
        protected virtual bool LocalizedFileExists(ControllerContext controllerContext, string fallbackVirtualPath, out string localizedVirtualPath) // DEGT Localization
        {
            bool localizedFileExists = false;
            int extPoint = fallbackVirtualPath.LastIndexOf(".");

            localizedVirtualPath = fallbackVirtualPath; // the same
            if (extPoint < fallbackVirtualPath.Length - 1)
            {
                string[] langs = controllerContext.HttpContext.Request.UserLanguages;   // From browser Accept-Language header
                if (langs.Length > 0)
                {
                    foreach (string code in langs)
                    {   // get language code without Q setting from Browser Accept-Language header
                        string langCode = code.Split(';')[0];   // remove ;q=N.N from preference when multiple languages given in browser
                        // convert Index.cshtml into Index.es-ES.cshtml
                        localizedVirtualPath = fallbackVirtualPath.Insert(extPoint, string.Format(".{0}", langCode));
                        localizedFileExists = FileExists(controllerContext, localizedVirtualPath);
                        if (localizedFileExists)
                            break;
                    }
                }

            }
            return localizedFileExists;
        }
#endif
    }

    public class AreaAwareViewLocation : ViewLocation
    {
        public AreaAwareViewLocation(string virtualPathFormatString)
            : base(virtualPathFormatString)
        {
        }

        public override string Format(string viewName, string controllerName, string areaName, string theme)
        {
            return string.Format(CultureInfo.InvariantCulture, _virtualPathFormatString, viewName, controllerName,
                areaName, theme);
        }
    }

    public class ViewLocation
    {
        protected readonly string _virtualPathFormatString;

        public ViewLocation(string virtualPathFormatString)
        {
            _virtualPathFormatString = virtualPathFormatString;
        }

        public virtual string Format(string viewName, string controllerName, string areaName, string theme)
        {
            return string.Format(CultureInfo.InvariantCulture, _virtualPathFormatString, viewName, controllerName, theme);
        }
    }
}
